/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hop.core.injection.bean;

import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.lang.StringUtils;
import org.apache.hop.core.injection.Injection;
import org.apache.hop.core.injection.InjectionDeep;
import org.apache.hop.core.injection.InjectionTypeConverter;
import org.apache.hop.metadata.api.IStringObjectConverter;

/** Storage for one transform on the bean deep level. */
public class BeanLevelInfo<Meta extends Object> {
  public enum DIMENSION {
    NONE,
    ARRAY,
    LIST,
  };

  /** Parent transform or null for root. */
  public BeanLevelInfo parent;

  /** Class for transform from field or methods. */
  public Class<Meta> leafClass;

  /** Field of transform, or null if bean has getter/setter. */
  public Field field;

  /** Getter and setter. */
  public Method getter;

  public Method setter;

  /** Dimension of level. */
  public DIMENSION dim = DIMENSION.NONE;

  /** Values converter. */
  public InjectionTypeConverter converter;

  /** Convert Strings (XML, JSON, ...) to the target object */
  public IStringObjectConverter stringObjectConverter;

  /** False if source empty value shouldn't affect on target field. */
  public boolean convertEmpty;

  /** Name on the path : prefix + key usually. */
  public String nameKey;

  /** Flag for the metadata property system with support for simplified List<String> fields */
  public boolean stringList = false;

  /** To support inflation from a hop metadata provider by name */
  public boolean storeWithName = false;

  public void init(BeanInjectionInfo info) {
    introspect(info, leafClass, new TreeMap<>());
  }

  public void init(BeanInjectionInfo info, Map<String, Type> ownerGenericsInfo) {
    introspect(info, leafClass, ownerGenericsInfo);
  }

  /** Introspect class and all interfaces and ancestors recursively. */
  private void introspect(BeanInjectionInfo info, Type type, Map<String, Type> ownerGenericsInfo) {
    Map<String, Type> genericsInfo = new TreeMap<>(ownerGenericsInfo);

    while (type != null) {
      Class<Meta> clazz;
      ParameterizedType pt;
      if (type instanceof ParameterizedType) {
        pt = (ParameterizedType) type;
        clazz = (Class<Meta>) pt.getRawType();
      } else {
        pt = null;
        clazz = (Class<Meta>) type;
      }
      // introspect generics
      TypeVariable<?>[] tps = clazz.getTypeParameters();
      if (tps.length > 0) {
        if (pt == null) {
          throw new RuntimeException("Can't introspect class with parameters on the high level");
        }
        Type[] args = pt.getActualTypeArguments();
        if (tps.length != args.length) {
          throw new RuntimeException("Wrong generics declaration");
        }
        Map<String, Type> prevGenerics = genericsInfo;
        genericsInfo = new TreeMap<>();
        for (int i = 0; i < tps.length; i++) {
          if (args[i] instanceof TypeVariable) {
            TypeVariable<?> argsi = (TypeVariable<?>) args[i];
            Type prev = prevGenerics.get(argsi.getName());
            if (prev == null) {
              throw new RuntimeException("Generic '" + args[i] + "' was not declared yet");
            }
            genericsInfo.put(tps[i].getName(), prev);
          } else {
            genericsInfo.put(tps[i].getName(), args[i]);
          }
        }
      }

      introspect(info, clazz.getDeclaredFields(), clazz.getDeclaredMethods(), genericsInfo);
      for (Type intf : clazz.getGenericInterfaces()) {
        introspect(info, intf, genericsInfo);
      }
      type = clazz.getGenericSuperclass();
    }
  }

  /** Introspect fields and methods of some class. */
  protected void introspect(
      BeanInjectionInfo info, Field[] fields, Method[] methods, Map<String, Type> genericsInfo) {
    for (Field f : fields) {
      Injection annotationInjection = f.getAnnotation(Injection.class);
      InjectionDeep annotationInjectionDeep = f.getAnnotation(InjectionDeep.class);
      if (annotationInjection == null && annotationInjectionDeep == null) {
        // no injection annotations
        continue;
      }
      if (annotationInjection != null && annotationInjectionDeep != null) {
        // both annotations exist - wrong
        throw new RuntimeException("Field can't be annotated twice for injection " + f);
      }
      if (f.isSynthetic() || f.isEnumConstant() || Modifier.isStatic(f.getModifiers())) {
        // fields can't contain real data with such modifier
        throw new RuntimeException("Wrong modifier for annotated field " + f);
      }
      BeanLevelInfo leaf = new BeanLevelInfo();
      leaf.parent = this;
      leaf.field = f;
      Type t;
      if (f.getType().isArray()) {
        Type ff = f.getGenericType();
        leaf.dim = DIMENSION.ARRAY;
        if (ff instanceof GenericArrayType) {
          GenericArrayType ffg = (GenericArrayType) ff;
          t = resolveGenericType(ffg.getGenericComponentType(), genericsInfo);
        } else {
          t = f.getType().getComponentType();
        }
      } else if (List.class.equals(f.getType())) {
        leaf.dim = DIMENSION.LIST;
        Type fieldType = f.getGenericType();
        Type listType = ((ParameterizedType) fieldType).getActualTypeArguments()[0];
        try {
          t = resolveGenericType(listType, genericsInfo);
        } catch (Throwable ex) {
          throw new RuntimeException("Can't retrieve type from List for " + f, ex);
        }
      } else {
        leaf.dim = DIMENSION.NONE;
        t = resolveGenericType(f.getGenericType(), genericsInfo);
      }
      if (t instanceof ParameterizedType) {
        ParameterizedType pt = (ParameterizedType) t;
        leaf.leafClass = (Class<?>) pt.getRawType();
      } else {
        leaf.leafClass = (Class<?>) t;
      }
      if (annotationInjection != null) {
        try {
          leaf.converter = annotationInjection.converter().getConstructor().newInstance();
        } catch (Exception ex) {
          throw new RuntimeException("Error instantiate converter for " + f, ex);
        }
        leaf.convertEmpty = annotationInjection.convertEmpty();
        info.addInjectionProperty(annotationInjection, leaf);
      } else if (annotationInjectionDeep != null) {
        // introspect deeper
        leaf.nameKey =
            calculateNameKey(
                annotationInjectionDeep.prefix(),
                annotationInjection != null ? annotationInjection.name() : "");
        TreeMap<String, Type> gi = new TreeMap<>(genericsInfo);
        leaf.introspect(info, t, gi);
      }
    }
    for (Method m : methods) {
      Injection annotationInjection = m.getAnnotation(Injection.class);
      InjectionDeep annotationInjectionDeep = m.getAnnotation(InjectionDeep.class);
      if (annotationInjection == null && annotationInjectionDeep == null) {
        // no injection annotations
        continue;
      }
      if (annotationInjection != null && annotationInjectionDeep != null) {
        // both annotations exist - wrong
        throw new RuntimeException("Method can't be annotated twice for injection " + m);
      }
      if (m.isSynthetic() || Modifier.isStatic(m.getModifiers())) {
        // method is static
        throw new RuntimeException("Wrong modifier for anotated method " + m);
      }
      BeanLevelInfo leaf = new BeanLevelInfo();
      leaf.parent = this;
      if (annotationInjectionDeep != null) {
        Type getterClass = isGetter(m);
        if (getterClass == null) {
          throw new RuntimeException("Method should be getter: " + m);
        }
        if (m.getReturnType() != null && List.class.equals(m.getReturnType())) {
          // returns list
          leaf.dim = DIMENSION.LIST;
          ParameterizedType getterType = (ParameterizedType) getterClass;
          getterClass = getterType.getActualTypeArguments()[0];
        }
        Class<?> getter = (Class<?>) resolveGenericType(getterClass, genericsInfo);
        if (getter.isArray()) {
          throw new RuntimeException("Method should be getter: " + m);
        }
        leaf.getter = m;
        leaf.leafClass = getter;
        leaf.nameKey =
            calculateNameKey(
                annotationInjectionDeep.prefix(),
                annotationInjection != null ? annotationInjection.name() : "");
        leaf.init(info);
      } else {
        Class<?> setterClass = isSetter(m);
        if (setterClass == null || setterClass.isArray()) {
          throw new RuntimeException("Method should be setter: " + m);
        }
        leaf.setter = m;
        leaf.leafClass = setterClass;
        try {
          leaf.converter = annotationInjection.converter().getConstructor().newInstance();
        } catch (Exception ex) {
          throw new RuntimeException("Error instantiate converter for " + m, ex);
        }
        leaf.convertEmpty = annotationInjection.convertEmpty();
        info.addInjectionProperty(annotationInjection, leaf);
      }
    }
  }

  private String calculateNameKey(String prefix, String name) {
    if (StringUtils.isEmpty(prefix)) {
      return name;
    } else {
      if (StringUtils.isEmpty(name)) {
        return prefix;
      } else {
        return prefix + "." + name;
      }
    }
  }

  /** Resolves class by generic map if required(when type is TypeVariable). */
  private Type resolveGenericType(Type type, Map<String, Type> genericsInfo) {
    if (type instanceof TypeVariable) {
      String name = ((TypeVariable<?>) type).getName();
      type = genericsInfo.get(name);
      if (type == null) {
        throw new RuntimeException("Unknown generics for '" + name + "'");
      }
    }
    return type;
  }

  private Type isGetter(Method m) {
    if (m.getReturnType() == void.class) {
      return null;
    }
    if (m.getParameterTypes().length == 0) {
      // getter without parameters
      return m.getGenericReturnType();
    }
    return null;
  }

  private Class<?> isSetter(Method m) {
    if (m.getReturnType() != void.class) {
      return null;
    }
    if (m.getParameterTypes().length == 1) {
      // setter with one parameter
      return m.getParameterTypes()[0];
    }
    return null;
  }

  protected List<BeanLevelInfo> createCallStack() {
    List<BeanLevelInfo> stack = new ArrayList<>();
    BeanLevelInfo p = this;
    while (p != null) {
      if (p.field != null) {
        p.field.setAccessible(true);
      }
      stack.add(p);
      p = p.parent;
    }
    Collections.reverse(stack);
    return stack;
  }

  @Override
  public String toString() {
    String r = "";
    if (field != null) {
      r += "field " + field.getName();
    } else {
      r += "<root field>";
    }
    r += "(class " + leafClass.getSimpleName() + ")";
    return r;
  }
}
